Obsidian 最大的賣點之一就是雙向連結,而這個功能特別仰賴 [[note-name]]
這樣的格式,讓我們能夠快速輸入。但在〈如何將 Obsidian 的附件跟著 Nuxt Content 發布〉時,因為 Nuxt Content 使用的 MDC 並沒有支援這樣的語法,所以只能先將其停用,今天就來實作吧。
今天所有實作的程式碼都會放在 @/server/plugins/wikilink.ts
裡,但為了方便解釋,所以會將程式碼分段去解說。
首先建立一篇文章用來證明我們渲染的成果是否符合預期,這邊是建立 @/content/sandbox/wikilink.md
:
---
title: Wiki Link 渲染示例
created_at: 2023-10-07T23:22:42.800+08:00
published_at: 2023-10-07T23:22:56.834+08:00
tags: []
---
## Text Link Testing:
### Regular Case
```
- Expected:
- single `[]`: [/dev/testcases/note]
- double `[]`: [note](/dev/testcases/note)
- double `[]` with alias: [My Note](/dev/testcases/note)
- Actual:
- single `[]`: [/dev/testcases/note]
- double `[]`: [[/dev/testcases/note]]
- double `[]` with alias: [[/dev/testcases/note|My Note]]
```
- Expected:
- single `[]`: [/dev/testcases/note]
- double `[]`: [note](/dev/testcases/note)
- double `[]` with alias: [My Note](/dev/testcases/note)
- Actual:
- single `[]`: [/dev/testcases/note]
- double `[]`: [[/dev/testcases/note]]
- double `[]` with alias: [[/dev/testcases/note|My Note]]
### Special Case Testing
#### Number
```
- Expected:
- [100](</dev/testcases/100>)
- Actually:
- [[/dev/testcases/100|100]]
```
- Expected:
- [100](</dev/testcases/100>)
- Actually:
- [[/dev/testcases/100|100]]
#### Han
```
- Expected:
- [測試](</dev/testcases/%E6%B8%AC%E8%A9%A6>)
- [測試](</dev/testcases/測試>)
- Actually:
- [[/dev/testcases/%E6%B8%AC%E8%A9%A6|測試]]
- [[/dev/testcases/測試|測試]]
```
- Expected:
- [測試](</dev/testcases/%E6%B8%AC%E8%A9%A6>)
- [測試](</dev/testcases/測試>)
- Actually:
- [[/dev/testcases/%E6%B8%AC%E8%A9%A6|測試]]
- [[/dev/testcases/測試|測試]]
#### Symbol
```
- Expected:
- dot: [test.with.space](</dev/testcases/test.with.space>)
- dash: [test-with-space](</dev/testcases/test-with-space>)
- underscore: [test_with_space](</dev/testcases/test_with_space>)
- Actually:
- dot: [[/dev/testcases/test.with.space|test.with.space]]
- dash: [[/dev/testcases/test-with-space|test-with-space]]
- underscore: [[/dev/testcases/test_with_space|test_with_space]]
```
- Expected:
- dot: [test.with.space](</dev/testcases/test.with.space>)
- dash: [test-with-space](</dev/testcases/test-with-space>)
- underscore: [test_with_space](</dev/testcases/test_with_space>)
- Actually:
- dot: [[/dev/testcases/test.with.space|test.with.space]]
- dash: [[/dev/testcases/test-with-space|test-with-space]]
- underscore: [[/dev/testcases/test_with_space|test_with_space]]
#### Space with URL encoding
```
- Expected:
- Space: [test with space](</dev/testcases/test%20with%20space>)
- Actually:
- Space: [[/dev/testcases/test%20with%20space|test with space]]
```
- Expected:
- Space: [test with space](</dev/testcases/test%20with%20space>)
- Actually:
- Space: [[/dev/testcases/test%20with%20space|test with space]]
#### Url enclosed in angle brackets
```
- Expected: [angle](</dev/testcases/angle>)
- Actually: [[</dev/testcases/angle>|angle]]
```
- Expected: [angle](</dev/testcases/angle>)
- Actually: [[</dev/testcases/angle>|angle]]
#### Han
```
- Expected:
- Space: [測試](</dev/testcases/測試>)
- Actually:
- Space: [[/dev/testcases/測試|測試]]
```
- Expected:
- Space: [測試](</dev/testcases/測試>)
- Actually:
- Space: [[/dev/testcases/測試|測試]]
#### Mixed
```
- Expected:
- [測-試_mix.ed 的.情%20況](</dev/testcases/測-試_mix.ed 的.情%20況>)
- Actually
- [[/dev/testcases/測-試_mix.ed 的.情%20況|測-試_mix.ed 的.情%20況]]
```
- Expected:
- [測-試_mix.ed 的.情%20況](</dev/testcases/測-試_mix.ed 的.情%20況>)
- Actually
- [[/dev/testcases/測-試_mix.ed 的.情%20況|測-試_mix.ed 的.情%20況]]
接著開始編寫程式碼
首先定義一個 Nitro Plugin,並建立一個 Hook,透過 'content:file:beforeParse'
這個 Hook,會讓內容檔案在被程式去解析前執行我們下方的函式。在這邊我們先做一個檢查,只要是 .md
結尾的檔案,其內文都會被 convertWikiLink()
轉換。
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('content:file:beforeParse', (file) => {
if (file._id.endsWith('.md')) {
file.body = convertWikiLink(file.body)
}
})
})
接著實作 convertWikiLink()
,這邊有個特別注意的是,就是透過 isInCodeBlock
變數來判斷我們現在是不是在 code block,如果是的話,就不會做任何事,如果不是,就會將那行透過 convertLinkMarkdown()
進行轉換。
function convertWikiLink(text: string): string {
let isInCodeBlock = false
const convertedLines = text.split('\n').map((line) => {
const isCodeBlockSyntax = line.startsWith('```') || line.startsWith('~~~')
isInCodeBlock = isCodeBlockSyntax ? !isInCodeBlock : isInCodeBlock
if (!isInCodeBlock) {
line = convertLinkMarkdown(line)
}
return line
})
return convertedLines.join('\n')
}
接著在 convertLinkMarkdown()
中,首先會透過 generateWikiLinkRegExp()
去取得我們要使用的正則表達式,然後開始去轉換,其中正則表達式會擷取兩個變數,一個是聯結路徑,一個是別名,透過這個方式協助我們重組為 Markdown 連結。
function convertLinkMarkdown(line: string) {
const regExp = generateWikiLinkRegExp()
return line.replaceAll(regExp, (_, linkPath, linkAlias) => {
const isExist = linkPath.startsWith('/')
const filename = linkPath.split('/').pop()
const unExistNoteLink = linkAlias || linkPath
const linkMarkdown = `[${linkAlias || filename}](<${encondingNoneAlphabetUrl(linkPath)}>)`
return isExist ? linkMarkdown : unExistNoteLink
})
}
在 generateWikiLinkRegExp()
中,會將中日韓英四國文字以及連結會用到的百分比、空白都涵蓋在判斷的範圍。
const generateWikiLinkRegExp = function () {
const regExpSets: RegExp[] = [
/\u4E00-\u9FFF/, // han: 中文漢字的 Unicode 範圍
/\u3400-\u4DBF/, // hanExtend: 中文擴展 A 的 Unicode 範圍
/\u3040-\u30FF/, // jpKana: 日文平假名和片假名的 Unicode 範圍
/\uAC00-\uD7AF/, // krHangul: 韓文字母的 Unicode 範圍
/\w\-./, // enCommon: 所有的英文字母、數字、底線字元、破折號、小數點
/%\\\//, // symbol: 百分比字元、斜線與反斜線字元。
/\s/, // space: 空白字元(如空格和 Tab 字元)
/<>/, // angle: 角括號
]
const pathSets = regExpSets.map(reg => reg.source).join('')
const pathPattern = `[${pathSets}]+`
const aliasPattern = /[^\[\]]+/.source
const re = `\\[\\[\\<?(${pathPattern})\\>?(?:\\|(${aliasPattern}))?\\]\\]`
return new RegExp(re, 'g')
}
如此就實作完成了,成果如下圖: